package day0803API.WebServer;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

//3.用HttpServletRequest解析请求数据
public class HttpServletRequest {
    //第一部分： 请求行
    //请求行的三部分：  请求方式，   uri抽象路径，  协议版本
    private  String method;
    private String uri;
    private String protocol;

    //第二部分：消息头(若干个key和value)
    Map<String,String> headers=new HashMap<>();


    private Socket socket;
    HttpServletRequest(Socket socket){
        this.socket=socket;
        //1.1.从socket中获取请求行的信息
        getRequestLine();
        //1.2.从socket中获取消息头的信息
        getRequestHeaders();

        //1.3.从socket中获取请求正文的信息
        getRequestContent();
    }

    //浏览器提交数据两种方式：
    //get  将浏览器提交的请求数据(请求参数)拼接在url后边
    //post 将浏览器提交的请求数据(请求参数)放在请求正文中发送
    private void getRequestContent() {
        //三个条件：
        // 数据提交方式需要时post方式，
        // 请求头中包含数据的长度(提交方式是post，并且提交数据了)
        // 提交数据的格式是  application/x-www-form-urlencoded
        // username=zs  &  password=123
        if("post".equalsIgnoreCase(method)){
            if(headers.containsKey("Content-Length")){
                int contentLength=Integer.parseInt(headers.get("Content-Length"));
                try {
                    InputStream is = socket.getInputStream();
                    //创建一个byte数组长度就是请求正文的长度(和请求正文中的请求数据长度相同)
                    //原因： 用输入流一次性将请求正文中的请求数据全部读取到byte数组中
                    byte[] bs = new byte[contentLength];
                    is.read(bs);
                    //content正文  type格式:  正文格式
                    String contentType=headers.get("Content-Type");
                    if("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)){
                        //用一个parseParameter()解析bs数组中的请求参数
                        //服务端的ServerSocket默认采用  ISO8859-1接收数据
                        //将byte[]   bs  采用ISO8859-1编码格式构建成一个String字符串
                        String line=new String(bs,  StandardCharsets.ISO_8859_1);
                        parseParameter(line);
                    }
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 解决传递中文问题
     *
     * 以登录页面为例，当我们在输入框中输入中文后点击登录，浏览器地址栏上可能看到的参数还是中文，但实际传递到服务端时，我们
     * 解析请求行的抽象路径得到参数时，样子如下:
     * /myweb/login?username=%E8%8C%83%E4%BC%A0%E5%A5%87&password=123456
     * 注:我们在用户名输入框里输入的是中文"范传奇"
     *
     * 为什么?
     * 原因:提交表单时，抽象路径会被包含在请求的请求行中。
     * 例如
     * GET /myweb/login?username=%E8%8C%83%E4%BC%A0%E5%A5%87&password=123456 HTTP/1.1
     *
     * HTTP协议要求请求的请求行和消息头都是字符串，且使用的文字对应的字符集只能是ISO8859-1.这是一个欧洲的字符集。里面就
     * 不支持中文字符。
     *
     *
     * 怎么办?
     * 曲线救国，想办法将不支持的中文用ISO8859-1支持的字符描述并传递给服务端。
     * 首先:
     * ISO8859-1支持数字。
     * 那么我们可以在浏览器中先将中文用UTF-8编码转换为2进制。
     *
     * GET /myweb/login?username=范&password=123456 HTTP/1.1   不要允许!!!!
     *
     * 例如:
     * "范"----UTF-8---->11101000 10001100 10000011(3个字节)
     *
     * 将2进制的1和0表达中文进行传递:
     * GET /myweb/login?username=111010001000110010000011&password=123456 HTTP/1.1
     * 虽然问题得以解决。副作用是:太长!!!!!  1中文字符就需要24个1或0的字符来表达。
     *
     *
     * 怎么缩短长度?
     * 解决办法:将2进制用16进制表达，仍然可以满足需求。因为16进制可以用字符'0'-'9'和'A'-'F'
     *
     * 2进制         10进制        16进制
     * 0000            0           0
     * 0001            1           1
     * 0010            2           2
     * 0011            3           3
     * 0100            4           4
     * 0101            5           5
     * 0110            6           6
     * 0111            7           7
     * 1000            8           8
     * 1001            9           9
     * 1010            10          A
     * 1011            11          B
     * 1100            12          C
     * 1101            13          D
     * 1110            14          E
     * 1111            15          F
     *
     * 换成16进制后的样子:
     * 11101000 10001100 10000011  "范"的UTF-8编码内容
     * E8       8C       83
     *
     * GET /myweb/login?username=E88C83&password=123456 HTTP/1.1
     * 原来需要用24个'0'或'1'字符的组合，现在仅需要6个字符就可以表达一个中文了
     *
     * 长度问题尽可能解决了，但是新的问题:如何与实际的英文数组组合区分开呢?
     * 例如:
     * 一个用于注册时取名字就叫:E88C83
     * 此时，提交表单，地址为:GET /myweb/login?username=E88C83&password=123456 HTTP/1.1
     *
     * 服务端接收到该内容时:如何理解:E88C83
     * 是当它为16进制数据，换算为2进制后用UTF-8还原为"范"
     * 还是本来就是E88C83
     *
     * 为了解决混淆问题，URL地址格式要求，如果英文数字组合表达的是16进制内容，则每两位16进制前必须添加一个'%'
     * 因此，实际传递中文名时应该如下:
     * GET /myweb/login?username=%E8%8C%83&password=123456 HTTP/1.1
     *
     * 如果时本来的英文数字组合应该如下:
     * GET /myweb/login?username=E88C83&password=123456 HTTP/1.1
     *
     * 揣测一个问题.如果改用输入的名字就叫:%E8%8C%83
     * 这里实际上像:'%','?','=','&'这些字符都属于URL的关键字符。当我们输入该字符传递时，浏览器会将该字符对应的1字节内容
     * 也以"%XX"转换。
     *
     *
     * 在服务端进行反向还原时，直接使用java提供的API:
     * URLDecoder.decode()就可以转换了。
     * decode:解码
     *
     *
     * 代码中我们在HttpServletRequest的parseParameters方法中将参数用URLDecoder.decode(line,"UTF-8")解码即可(浏览器用utf8发送，服务端用utf8接收)
     * (上边讲解的那么多复杂过程，指的是： http协议只支持iso8859-1编码发送数据，所以utf8编码会用utf8和iso8859-1同时支持的符号发送<用16进制数据表示中文，每个数据用分隔%>)
     * (如果真实数据中包含% 则utf8也会将将%转化为   "%十六进制数据" 的格式)
     */
    Map<String,String>  parameters=new HashMap<>();
    private void parseParameter(String line) {
        //用支持中文的编码方式UTF8重新将String字符串String line构建为另一个字符串(支持中文的)
        //调用工具类URLDecoder类的. decode()解码方法   :将同样的文字数据采用另外一种编排方式重新组织一下(原来采用的是ISO_8859_1组织方式)
        try {
            line= URLDecoder.decode(line, "UTF-8");
            String[] params=line.split("&");   //post方式提交的数据，    请求正文内：  username=zs   &   pwd = 123
            for(String ele : params){
                String[]  aParam=ele.split("=", 2);  //最多切分(2-1)次
                parameters.put(aParam[0],aParam[1]);
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }


    private void getRequestHeaders() {
        while(true){
            String aHeaderStr=readLine();
            if(aHeaderStr.isEmpty()){//如果本行读取的是空(空字符串)则表示消息头数据读取完毕就结训循环
                break;
            }
            String []  keyVal=aHeaderStr.split(":\\s");
            headers.put(keyVal[0],keyVal[1]);
            System.out.println(keyVal[0]+":"+keyVal[1]);
        }
    }

    //从socket中获取输入流，用输入读取一行文字，切分出三部分，分别 赋值给三个成员变量method,uri,protocol
    private void getRequestLine() {
        String requestLine=readLine();
        //切分这个字符串requestLine为三部分
        String[]  requestLineArr=requestLine.split("\\s");
        method=requestLineArr[0];
        uri=requestLineArr[1];
        protocol=requestLineArr[2];
        System.out.println(method+"--"+uri+"---"+protocol);
    }

    private String readLine() {
        StringBuilder stringBuilder=new StringBuilder();
        try {
            InputStream is=socket.getInputStream();
            int data=-1;
            char pre='a';
            char now='a';
            while( (data=is.read())!=-1){
                now=(char)data;
                if(pre==13&&now==10){
                    break;
                }
                stringBuilder.append(now);
                pre=now;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return  stringBuilder.toString().trim();
    }

    public String getMethod() {
        return method;
    }

    public void setMethod(String method) {
        this.method = method;
    }

    public String getUri() {
        return uri;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }

    public String getProtocol() {
        return protocol;
    }

    public void setProtocol(String protocol) {
        this.protocol = protocol;
    }

    public Socket getSocket() {
        return socket;
    }

    public void setSocket(Socket socket) {
        this.socket = socket;
    }
}
